Skip to content

feat: opt-in pipeline fit optimisations, native bucketize, and sampled fitting support with caching - #69

Open
ConorWorthington wants to merge 31 commits into
ExpediaGroup:mainfrom
ConorWorthington:main
Open

feat: opt-in pipeline fit optimisations, native bucketize, and sampled fitting support with caching#69
ConorWorthington wants to merge 31 commits into
ExpediaGroup:mainfrom
ConorWorthington:main

Conversation

@ConorWorthington

Copy link
Copy Markdown
Contributor

Description

Provide a short description of the PR changes.

The below checklists come from the docs page on adding new transformers here

Keras Layer Checklist

Verify that:

  • The new Keras layer extends BaseLayer
  • The _call method has been implemented in the new layer.
  • The compatible_dtypes property is defined in the new layer.
  • The new layer is decorated with @tf.keras.utils.register_keras_serializable(package=kamae.__name__).
  • The new layer takes a name, input_dtype, and output_dtype as arguments to the constructor and that this is passed to the super constructor.
  • The Keras layer is serializable. I have implemented the get_config method.
  • There are unit tests of the new layer.
  • There is a specific test of layer serialisation added here.
  • The new layer is imported in the init.py file in the layers directory.

Spark Transformer/Estimator Checklist

Verify that:

  • The new Spark Transformer extends BaseTransformer.
  • If the new transform needs a fit method, a Spark Estimator has been implemented that extends BaseEstimator.
  • The instructions in the above docs page have been followed for the __init__ and setParams methods.
  • The transformer uses one of the input/output mixin classes from base.py.
  • If the new transformer requires more parameters that would need to be serialised to the Spark ML pipeline, there is a implemented parameter class by extending the Params class here.
  • The compatible_dtypes property has been implemented to specify the input/output data types that my transformer/estimator supports.
  • A Keras subclassed layer is returned in the transformer's get_tf_layer method.
  • There are unit tests of the new transform. In particular, there are parity tests between the Spark and Keras implementations.
  • The new transformer/estimator is imported in the init.py file in the transformers/estimators directory.

Finally, please verify that:

  • There is a new entry (alphabetical order) in the README table describing the new layer/transformer

ConorWorthington and others added 24 commits June 29, 2026 16:31
Plans are too slow to materialise - this is our attempt to speed it up
Wrap the moments aggregation in StandardScale, SingleFeatureArrayStandardScale
and ConditionalStandardScale estimators in a guarded persist/unpersist so the
array-size probe and the aggregation reuse a materialised result instead of
re-scanning the upstream lineage twice. Repair the incomplete persist edit in
ConditionalStandardScale._fit.

Add checkpointInterval / pruneInputColumns coverage to the pipeline tests and a
checkpoint directory to the spark_session fixture. Surface estimator fit errors
as RuntimeError chained from the original exception.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@ConorWorthington
ConorWorthington requested a review from a team as a code owner August 5, 2026 13:56
@ConorWorthington ConorWorthington changed the title feat: opt-in pipeline fit optimisations, native bucketize, and sampled fitting feat: opt-in pipeline fit optimisations, native bucketize, and sampled fitting support with caching Aug 5, 2026

@georyetti georyetti left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mostly minor changes to pipeline logic. Also bucketize changes I assume should not be here. Lastly you need to run a uv lock.

Comment thread src/kamae/spark/pipeline/pipeline.py Outdated
"""
super().__init__(stages=stages)
kwargs = self._input_kwargs
super().__init__()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new super call drops the stages=stages, was this intended?

Comment thread src/kamae/spark/pipeline/pipeline.py Outdated
kwargs = self._input_kwargs
super().__init__()
self._setDefault(
checkpointInterval=0,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Default to None instead of 0?

Comment on lines +271 to +282
for param in stage.params:
if not (param.name.endswith("Col") or param.name.endswith("Cols")):
continue
if not stage.isDefined(param):
continue
value = stage.getOrDefault(param)
if isinstance(value, str):
required_input_columns.add(value)
elif isinstance(value, (list, tuple)):
required_input_columns.update(
item for item in value if isinstance(item, str)
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels more complex than I think it needs to be. Two things:

  1. Why do we need to iterate through the stage.params at all if we have already added the inputs using get_layer_inputs_outputs
  2. Even if we do need, we can just check for presence of inputCol or inputCols as a stage always has strictly one of these defined. if stage.hasParam("inputCol") and stage.isDefined("inputCol"): value = stage.getInputCol() and after check input cols. But this is really what get_layer_inputs_outputs does.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right that inputCol/inputCols are already covered by get_layer_inputs_outputs but the sweep isn't for those.

It's there for the other column params a stage reads that aren't its input col: maskCols/relevanceCol on ConditionalStandardScaleEstimator, and queryIdCol on the listwise transformers. Those get read at fit time, so if we don't keep them, pruneInputColumns=True drops them and the fit blows up with "column not found". There are regression tests covering exactly that.

If the suffix-matching feels too hacky, I'm happy to swap it for a small get_fit_input_columns() hook on the handful of stages that need it instead. Let me know what you'd prefer.

Comment thread src/kamae/spark/pipeline/pipeline.py Outdated
"""
required_input_columns = self.collect_required_input_columns(stages)
columns_to_keep = [c for c in dataset.columns if c in required_input_columns]
if columns_to_keep and len(columns_to_keep) < len(dataset.columns):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pedantic but do we need that second condition? If we are inside this function then we are pruning, and columns_to_keep is always a subset. So I would just check it's not empty and otherwise select

fit. 0 (or None) disables checkpointing.
:returns: KamaeSparkPipeline object with checkpointInterval set.
"""
return self._set(checkpointInterval=value)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should error on checkpoint interval being negative here. Personally I think we should error on 0 too and treat None as the no checkpoint behaviour

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated

Comment thread src/kamae/spark/pipeline/pipeline.py Outdated
:returns: KamaeSparkPipeline object with params set.
"""
kwargs = self._input_kwargs
return self._set(**kwargs)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This set params does not use the setter methods at all. So any validation in them will not be respected when the user passes arguments to the init as opposed to using the setter method. If you check how I defined the setParams for the estimator and transformer you can see I use the setter method.

return None
# We add 1 because we want to reserve the 0 index for mask/padding.
return bisect_right(splits, value) + 1
def bucketize(value: Column) -> Column:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same to assume that all this bucket logic changes to this transformer are not meant to be in this PR?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was looking for inefficiencies in the library... I was going to patch more but only bucketize was really impacted in a way I could speed up simply. We don't really use it but figured it was a good improvement so may as well include.

Comment thread pyproject.toml
dependencies = [
"pyspark>=3.4.0,<4.0.0",
"pandas>=1.3.4,<3.0.0",
"pyarrow>=4.0.0",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding new dependencies needs a uv lock pls

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lock updated

- Restore stages=stages in __init__ super call
- checkpointInterval defaults to None; reject non-positive via setter
- Route setParams through setter methods so validation runs
- Drop redundant length check in prune_unused_input_columns
- Regenerate uv.lock to include pyarrow (required by pandas_udf)

Retains the aux-column sweep in collect_required_input_columns: it is
load-bearing for pruning correctness (maskCols/relevanceCol/queryIdCol are
not returned by get_layer_inputs_outputs) and defended by regression tests.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants